import os
import curses
import struct
import threading
import queue
import configparser
import cantools
import can

# 用于存储接收到的 CAN 报文的队列
message_queue = queue.Queue()
# 控制线程运行的标志，当为 True 时，线程继续运行；当为 False 时，线程停止运行
running = True
# 用于保护 stdscr 的锁，确保在多线程环境下对 stdscr 的操作是线程安全的
stdscr_lock = threading.Lock()
# 全局 DBC 文件对象，用于存储加载的 DBC 文件信息
global_db = None

def check_files_and_folders():
    """
    检查必要的文件和文件夹是否存在。
    如果 'dbcfile' 文件夹或 'config.ini' 文件不存在，程序将输出错误信息并退出。
    """
    if not os.path.exists('dbcfile'):
        print("错误: 未找到 'dbcfile' 文件夹，请确保该文件夹存在。")
        exit(1)
    if not os.path.exists('config.ini'):
        print("错误: 未找到 'config.ini' 文件，请确保该文件存在。")
        exit(1)

def read_config():
    """
    读取配置文件 'config.ini' 中的配置信息。
    如果配置项不存在，则使用默认值。
    :return: 元组 (channel_name, bus_type, output)，分别表示 CAN 通道名称、总线类型和是否输出日志
    """
    config = configparser.ConfigParser()
    config.read('config.ini')
    channel_name = config.get('CAN', 'channel_name', fallback='can0')
    bus_type = config.get('CAN', 'bus_type', fallback='socketcan')
    output = config.getboolean('Display', 'output', fallback=False)

    return channel_name, bus_type, output

def get_single_dbc_file_path(path):
    """
    在指定路径下查找后缀为 .dbc 的文件。
    如果找到且只有一个 .dbc 文件，则返回该文件的路径；否则，输出错误信息并返回 None。
    :param path: 要查找的路径
    :return: 如果找到且只有一个 .dbc 文件，返回该文件的路径；否则返回 None
    """
    dbc_files = []
    for root, dirs, files in os.walk(path):
        for file in files:
            if file.endswith('.dbc'):
                dbc_files.append(os.path.join(root, file))
    if len(dbc_files) == 1:
        return dbc_files[0]
    else:
        print("Please ensure that there is one and only one file with the suffix dbc")
        return None

def load_dbc_file():
    """
    加载 DBC 文件并将其存储在全局变量 global_db 中。
    如果文件未找到或加载过程中出现问题，将输出相应的错误信息。
    """
    global global_db
    dbc_file_path = get_single_dbc_file_path("dbcfile")
    try:
        global_db = cantools.database.load_file(dbc_file_path)
    except FileNotFoundError:
        print(f"错误: 未找到 {dbc_file_path} 文件。")
    except Exception as e:
        print(f"错误: 加载 DBC 文件时出现问题: {e}")

def read_can_messages():
    """
    从 CAN 总线接收报文，并将其放入消息队列中。
    该方法在一个单独的线程中运行，直到 running 标志变为 False 或用户按下 Ctrl+C。
    """
    global running
    channel_name, bus_type, _ = read_config()
    bus = can.interface.Bus(channel=channel_name, bustype=bus_type)
    try:
        while running:
            message = bus.recv(timeout=1)
            if message is not None:
                message_queue.put(message)
    except KeyboardInterrupt:
        pass
    finally:
        bus.shutdown()

def show_message_only(stdscr, message_data, id):
    """
        仅显示指定 ID 的 CAN 消息详细信息。

        :param stdscr: curses 标准屏幕对象，用于显示信息
        :param message_data: 接收到的 CAN 消息数据
        :param id: 要显示的 CAN 消息的 ID
    """
    with stdscr_lock:
        hex_str = ''.join(format(byte, '02x') for byte in message_data.data)
        if global_db:
            height, width = stdscr.getmaxyx()
            y = 0
            stdscr.addstr(y, 0, "任意数字返回：")
            for message in global_db.messages:
                if y >= height:
                    break
                if message_data.arbitration_id == message.frame_id == id:
                    stdscr.addstr(1, 0, f"{message.frame_id:#x} {message.name} {message_data.timestamp} 0x{hex_str}")
                    decode_can_signal(stdscr, message_data)


def show_message(stdscr, message_data):
    """
        显示 CAN 消息的基本信息，并提示用户输入 ID 查看详情。

        :param stdscr: curses 标准屏幕对象，用于显示信息
        :param message_data: 接收到的 CAN 消息数据
        """
    with stdscr_lock:
        hex_str = ''.join(format(byte, '02x') for byte in message_data.data)
        if global_db:
            height, width = stdscr.getmaxyx()
            y = 0
            stdscr.addstr(y, 0, "输入 id 查看详情：")
            y += 1
            for message in global_db.messages:
                if y >= height:
                    break
                if message_data.arbitration_id == message.frame_id:
                    stdscr.addstr(y, 0, f"{message.frame_id:#x} {message.name} {message_data.timestamp} 0x{hex_str}")
                else:
                    stdscr.addstr(y, 0, f"{message.frame_id:#x} {message.name}")
                y += 1


def decode_can_signal(stdscr, message_data):
    """
      解码 CAN 消息的信号并显示在屏幕上。

      :param stdscr: curses 标准屏幕对象，用于显示信息
      :param message_data: 接收到的 CAN 消息数据
      """
    binary_str = ''.join(format(byte, '08b') for byte in message_data.data)
    message_id = message_data.arbitration_id
    try:
        byte_data = struct.pack('B' * (len(binary_str) // 8),
                                *[int(binary_str[i:i + 8], 2) for i in range(0, len(binary_str), 8)])
        message = global_db.get_message_by_frame_id(message_id)
        decoded_signals = message.decode(byte_data)
        y = 2
        for signal_name, value in decoded_signals.items():
            stdscr.addstr(y, 0, f"信号名称: {signal_name}, 物理值: {value}")
            y += 1
    except KeyError:
        print(f"错误: DBC 文件中未找到消息 ID 为 {message_id} 的消息。")
    except Exception as e:
        print(f"错误: 解码信号时出现问题: {e}")


def log_can_messages(message):
    """
    将 CAN 消息记录到日志文件中。

    :param message: 接收到的 CAN 消息
    """
    hex_str = ''.join(format(byte, '02x') for byte in message.data)
    log_entry = f"{message.arbitration_id:#x} {message.timestamp} 0x{hex_str}\n"
    with open('can_messages.log', 'a') as log_file:
        log_file.write(log_entry)


def display_messages(stdscr):
    """
    显示 CAN 消息，并处理用户输入。

    :param stdscr: curses 标准屏幕对象，用于显示信息
    """
    global running
    target_id = None
    input_buffer = ""
    _, _, output = read_config()
    while running:
        try:
            message = message_queue.get(timeout=2)
            if output:
                log_can_messages(message)
            if target_id is None:
                show_message(stdscr, message)
            else:
                show_message_only(stdscr, message, target_id)
        except queue.Empty:
            pass
        with stdscr_lock:
            stdscr.refresh()
        stdscr.nodelay(1)
        key = stdscr.getch()
        if key != -1:
            if key == ord('\n'):  # 回车键确认输入
                if input_buffer:
                    try:
                        if target_id is None:
                            target_id = int(input_buffer, 16)
                            with stdscr_lock:
                                stdscr.clear()
                        else:
                            target_id = None
                            with stdscr_lock:
                                stdscr.clear()  # 再次输入则恢复到原始循环
                    except ValueError:
                        pass
                    input_buffer = ""
            elif key == 263:  # 退格键删除输入
                input_buffer = input_buffer[:-1]
            elif chr(key).isdigit() or (chr(key).isalpha() and chr(key).lower() in 'abcdef'):
                input_buffer += chr(key)


def main(stdscr):
    # 隐藏光标，提升用户体验
    curses.curs_set(0)
    # 清空屏幕，准备显示信息
    stdscr.clear()

    # 加载 DBC 文件，用于解析 CAN 消息
    load_dbc_file()

    # 启动一个新线程来接收 CAN 报文
    # target 参数指定线程要执行的函数，即 read_can_messages 函数
    receive_thread = threading.Thread(target=read_can_messages)
    # 启动接收线程
    receive_thread.start()

    # 启动一个新线程来显示接收到的 CAN 消息
    # target 参数指定线程要执行的函数，即 display_messages 函数
    # args 参数传递给 display_messages 函数的参数，这里是 stdscr 用于显示信息
    display_thread = threading.Thread(target=display_messages, args=(stdscr,))
    # 启动显示线程
    display_thread.start()

    try:
        # 主线程等待接收线程结束
        receive_thread.join()
        # 主线程等待显示线程结束
        display_thread.join()
    except KeyboardInterrupt:
        # 如果用户按下 Ctrl+C，设置 running 标志为 False，通知线程停止运行
        global running
        running = False
        # 确保接收线程完全停止
        receive_thread.join()
        # 确保显示线程完全停止
        display_thread.join()


if __name__ == '__main__':
    check_files_and_folders()
    curses.wrapper(main)